package sistema;

import comandos.GestorDeComandos;
import comandos.IGestorComandos;
import entidades.Ancora;
import entidades.Cor;
import entidades.Decorador;
import entidades.Estilo;
import entidades.Familia;
import entidades.Telha;
import entidades.TelhaBase;
import entidades.Textura;
import entidades.TipoTelha;
import entidades.interfaces.ITelhaBaseVisitante;
import entidades.util.Grelha;
import estratégia.IMoverEstrategia;
import estratégia.MoverNormal;
import fabricas.estilos.FabricaDeFabricasDeEstilos;
import gestordeentidades.GestorDeEntidades;
import gestordeentidades.persistance.DataBaseObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import maquinaestados.Estado;
import maquinaestados.EstadoNormal;
import sistema.util.KeyTelha;

/**
 * Trabalho DAS - 2014/2015
 *
 * @author a21210380, a21190325, a21210392
 */
public class Sistema implements ISistema {

    private final ObjectProperty<Estado> estado = new SimpleObjectProperty<>(new EstadoNormal());

    private final Grelha grelha;
    private final IGestorComandos gestorComandos;
    private final Map<String, Familia> familias;
    private final Map<String, Estilo> estilos;
    private final ObservableMap<KeyTelha, TelhaBase> telhas;

    private IMoverEstrategia moverEstrategia;

    private Sistema(Map<String, Familia> familias, Map<String, Estilo> estilos, ObservableMap<KeyTelha, TelhaBase> telhas) {
        this.familias = familias;
        this.estilos = estilos;
        this.telhas = telhas;

        this.grelha = new Grelha();
        this.gestorComandos = new GestorDeComandos();
        this.moverEstrategia = new MoverNormal(grelha);
    }

    @Override
    public IGestorComandos getGestorComandos() {
        return gestorComandos;
    }

    @Override
    public boolean nomeDaFamiliaDisponivel(String nome) {
        if (nome == null) {
            throw new NullPointerException("nome não pode ser null!");
        }

        return GestorDeEntidades.getInstance().filtrarEntidades(Familia.class, (Familia e) -> {
            return e.getNome().equals(nome);
        }).isEmpty();
    }

    @Override
    public boolean addFamilia(Familia familia) {
        if (familia == null) {
            throw new NullPointerException("familia não pode ser null!");
        }

        if (!familias.containsKey(familia.getNome())) {
            familia.registarNovaEntidade();
            familias.put(familia.getNome(), familia);
            return true;
        }
        return false;
    }

    @Override
    public boolean editFamilia(Familia newFamilia, Familia oldFamilia) {
        if (newFamilia == null || oldFamilia == null) {
            throw new NullPointerException("familia não pode ser null!");
        }
        if (familias.containsKey(oldFamilia.getNome())
                && (!familias.containsKey(newFamilia.getNome()) || newFamilia.getNome().equals(oldFamilia.getNome()))) {
            Familia remove = familias.remove(oldFamilia.getNome());

            Cor corAux1 = remove.getCorLinhaDefault();
            remove.setCorLinhaDefault(newFamilia.getCorLinhaDefault());

            Cor corAux2 = remove.getCorTextoDefault();
            remove.setCorTextoDefault(newFamilia.getCorTextoDefault());

            Textura texturaAux = remove.getFundoDefault();
            remove.setFundoDefault(newFamilia.getFundoDefault());

            String nomeAux = remove.getNome();
            remove.setNome(newFamilia.getNome());

            if (!texturaAux.getCaminhoDoFicheiro().equals(newFamilia.getFundoDefault().getCaminhoDoFicheiro())) {
                if (!newFamilia.getFundoDefault().isRegistado()) {
                    newFamilia.getFundoDefault().registarNovaEntidade();
                }
            }
            oldFamilia.registarEntidadeModificada();
            familias.put(remove.getNome(), remove);

            newFamilia.setCorLinhaDefault(corAux1);
            newFamilia.setCorTextoDefault(corAux2);
            newFamilia.setFundoDefault(texturaAux);
            newFamilia.setNome(nomeAux);

            return true;
        }
        return false;
    }

    @Override
    public boolean removeFamilia(Familia familia) {
        if (familia == null) {
            throw new NullPointerException("familia não pode ser null!");
        }

        if (!familias.containsKey(familia.getNome())) {
            return false;
        }

        if (!estilos.values().stream().noneMatch((e) -> (e.getFamilia().getId() == familia.getId()))) {
            return false;
        }

        if (!telhas.values().stream().noneMatch((t) -> (t.getFamilia().getId() == familia.getId()))) {
            return false;
        }

        familia.registarEntidadeApagada();
        familias.remove(familia.getNome());
        return true;
    }

    @Override
    public Familia getFamilia(String nome) {
        if (nome == null) {
            throw new NullPointerException("nome não pode ser null!");
        }

        if (familias.containsKey(nome)) {
            return familias.get(nome);
        }
        return null;
    }

    @Override
    public Map<String, Familia> getFamilias() {
        return familias;
    }

    @Override
    public boolean nomeDoEstiloDisponivel(String nome) {
        if (nome == null) {
            throw new NullPointerException("nome não pode ser null!");
        }

        return GestorDeEntidades.getInstance().filtrarEntidades(Estilo.class, (Estilo e) -> {
            return e.getNomeDoEstilo().equals(nome);
        }).isEmpty();
    }

    @Override
    public boolean addEstilo(Estilo estilo) {
        if (estilo == null) {
            throw new NullPointerException("estilo não pode ser null!");
        }

        if (!estilos.containsKey(estilo.getNomeDoEstilo())) {
            if (!familias.containsKey(estilo.getFamilia().getNome())) {
                return false;
            }
            if (familias.get(estilo.getFamilia().getNome()).addEstilo(estilo)) {
                estilo.registarNovaEntidade();
                estilos.put(estilo.getNomeDoEstilo(), estilo);
                return true;
            }
        }
        return false;
    }

    @Override
    public Estilo addEstiloDeFamilia(Familia familia) {
        Estilo novoEstilo;
        if (familia == null) {
            throw new NullPointerException("estilo não pode ser null!");
        }

        if (familias.containsKey(familia.getNome())) {
            novoEstilo = FabricaDeFabricasDeEstilos.getEstilo(familia);
            novoEstilo.registarNovaEntidade();
            estilos.put(novoEstilo.getNomeDoEstilo(), novoEstilo);
            return novoEstilo;
        }
        return null;
    }

    @Override
    public Estilo addEstiloDeTelha(TelhaBase telha) {
        Estilo novoEstilo;
        if (telha == null) {
            throw new NullPointerException("estilo não pode ser null!");
        }
        KeyTelha key = new KeyTelha(telha.getId(), telha.getClass());
        if (telhas.containsKey(key)) {
            novoEstilo = FabricaDeFabricasDeEstilos.getEstilo(telha);
            novoEstilo.registarNovaEntidade();
            estilos.put(novoEstilo.getNomeDoEstilo(), novoEstilo);
            return novoEstilo;
        }
        return null;
    }

    @Override
    public boolean editEstilo(Estilo newEstilo, Estilo oldEstilo) {
        if (newEstilo == null || oldEstilo == null) {
            throw new NullPointerException("estilos não podem ser null!");
        }

        if (estilos.containsKey(oldEstilo.getNomeDoEstilo())
                && (!estilos.containsKey(newEstilo.getNomeDoEstilo()) || newEstilo.getNomeDoEstilo().equals(oldEstilo.getNomeDoEstilo()))) {
            Estilo remove = estilos.remove(oldEstilo.getNomeDoEstilo());

            Cor corAux1 = remove.getCorDaLinha();
            remove.setCorDaLinha(newEstilo.getCorDaLinha());

            Cor corAux2 = remove.getCorDoTexto();
            remove.setCorDoTexto(newEstilo.getCorDoTexto());

            Textura texturaAux = remove.getFundo();
            remove.setFundo(newEstilo.getFundo());

            String nomeAux = remove.getNomeDoEstilo();
            remove.setNomeDoEstilo(newEstilo.getNomeDoEstilo());
            estilos.put(remove.getNomeDoEstilo(), remove);

            if (!texturaAux.getCaminhoDoFicheiro().equals(newEstilo.getFundo().getCaminhoDoFicheiro())) {
                if (!newEstilo.getFundo().isRegistado()) {
                    newEstilo.getFundo().registarNovaEntidade();
                }
            }
            oldEstilo.registarEntidadeModificada();

            newEstilo.setCorDaLinha(corAux1);
            newEstilo.setCorDoTexto(corAux2);
            newEstilo.setFundo(texturaAux);
            newEstilo.setNomeDoEstilo(nomeAux);
            return true;
        }
        return false;
    }

    @Override
    public boolean removeEstilo(Estilo estilo) {
        if (estilo == null) {
            throw new NullPointerException("estilo não pode ser null!");
        }

        if (!estilos.containsKey(estilo.getNomeDoEstilo())) {
            return false;
        }
        if (familias.get(estilo.getFamilia().getNome()).removeEstilo(estilo)) {
            estilo.registarEntidadeApagada();
            estilos.remove(estilo.getNomeDoEstilo());
            return true;
        }
        return false;
    }

    @Override
    public Estilo getEstilo(String nome) {
        if (nome == null) {
            throw new NullPointerException("estilo não pode ser null!");
        }

        if (estilos.containsKey(nome)) {
            return estilos.get(nome);
        }
        return null;
    }

    @Override
    public Map<String, Estilo> getEstilos() {
        return estilos;
    }

    @Override
    public List<Estilo> getTodosEstilosDaFamilia(Familia familia) {
        if (familia == null) {
            throw new NullPointerException("familia não pode ser null!");
        }

        return GestorDeEntidades.getInstance().filtrarEntidades(Estilo.class, (Estilo e) -> {
            return e.getFamilia().equals(familia);
        });
    }

    @Override
    public boolean addTelha(TelhaBase telha) {
        if (telha == null) {
            throw new NullPointerException("telha não pode ser null!");
        }

        KeyTelha key = new KeyTelha(telha.getId(), telha.getClass());
        if (telhas.containsKey(key)) {
            return false;
        }

        telha.acceptChain(new ITelhaBaseVisitante() {

            @Override
            public void visitante(Telha t) {
                t.registarNovaEntidade();
                t.getRectangulo().registarNovaEntidade();
                t.getRectangulo().getOrigem().registarNovaEntidade();
                t.getDesenho().registarNovaEntidade();
                t.getAncoras().forEach((Ancora a) -> {
                    if (!a.isRegistado()) {
                        a.registarNovaEntidade();
                        a.getPonto().registarNovaEntidade();
                    }
                });
            }

            @Override
            public void visitante(Decorador d) {
                d.registarNovaEntidade();
                HashMap<Integer, DataBaseObject> valores = d.getValores();
                if (valores.containsKey(Decorador.VALOR_FUNDO)) {
                    ((Textura) valores.get(Decorador.VALOR_FUNDO)).registarNovaEntidade();
                }
            }
        });

        key = new KeyTelha(telha.getId(), telha.getClass());
        telhas.put(key, telha);

        return true;
    }

    @Override
    public boolean removeTelha(TelhaBase telha) {
        if (telha == null) {
            throw new NullPointerException("telha não pode ser null!");
        }

        KeyTelha key = new KeyTelha(telha.getId(), telha.getClass());
        if (!telhas.containsKey(key)) {
            return false;
        }
        telhas.remove(key);

        telha.acceptChain(new ITelhaBaseVisitante() {

            @Override
            public void visitante(Telha t) {
                t.registarEntidadeApagada();
                t.getRectangulo().registarEntidadeApagada();
                t.getRectangulo().getOrigem().registarEntidadeApagada();
                t.getDesenho().registarEntidadeApagada();
                t.getAncoras().forEach((Ancora a) -> {
                    if (!a.isRegistado()) {
                        a.registarEntidadeApagada();
                        a.getPonto().registarEntidadeApagada();
                    }
                });
            }

            @Override
            public void visitante(Decorador d) {
                d.registarEntidadeApagada();
                HashMap<Integer, DataBaseObject> valores = d.getValores();
                if (valores.containsKey(Decorador.VALOR_FUNDO)) {
                    ((Textura) valores.get(Decorador.VALOR_FUNDO)).registarEntidadeApagada();
                }
            }
        });

        return true;
    }

    @Override
    public TelhaBase getTelha(float x, float y) {
        for (TelhaBase t : telhas.values()) {
            if (t.getRectangulo().contem(x, y)) {
                return t;
            }
        }

        return null;
    }

    @Override
    public boolean decorarTelha(Decorador decorador) {
        if (decorador == null) {
            throw new NullPointerException("decorador não pode ser null!");
        }
        KeyTelha key = new KeyTelha(decorador.getId(), Decorador.class);
        if (telhas.containsKey(key)) {
            return false;
        }
        KeyTelha keyDecorada = new KeyTelha(decorador.getTelhaDecorada().getId(), decorador.getTelhaDecorada().getClass());
        if (telhas.containsKey(keyDecorada)) {
            decorador.registarNovaEntidade();
            key = new KeyTelha(decorador.getId(), Decorador.class);
            telhas.remove(keyDecorada);
            telhas.put(key, decorador);
            return true;
        }
        return false;
    }

    @Override
    public boolean removerDecoracaoDaTelha(Decorador decorador) {
        if (decorador == null) {
            throw new NullPointerException("decorador não pode ser null!");
        }
        KeyTelha key = new KeyTelha(decorador.getId(), Decorador.class);
        if (!telhas.containsKey(key)) {
            return false;
        }
        KeyTelha keyDecorada = new KeyTelha(decorador.getTelhaDecorada().getId(), decorador.getTelhaDecorada().getClass());
        if (!telhas.containsKey(keyDecorada)) {
            telhas.remove(key);
            telhas.put(keyDecorada, decorador.getTelhaDecorada());
            decorador.registarEntidadeApagada();
            return true;
        }
        return false;
    }

    @Override
    public ObservableMap<KeyTelha, TelhaBase> getTelhas() {
        return telhas;
    }

    @Override
    public TipoTelha getTipoDeTelha(String nome) {
        if (nome == null) {
            throw new NullPointerException("nome não pode ser null!");
        }

        List<TipoTelha> filtrarEntidades = GestorDeEntidades.getInstance().filtrarEntidades(TipoTelha.class, (TipoTelha c) -> {
            return c.getNome().equals(nome);
        });

        if (filtrarEntidades.isEmpty()) {
            TipoTelha c = new TipoTelha(nome);
            c.registarNovaEntidade();
            return c;
        }

        return filtrarEntidades.get(0);
    }

    @Override
    public Cor getCor(double r, double g, double b, double o) {
        List<Cor> filtrarEntidades = GestorDeEntidades.getInstance().filtrarEntidades(Cor.class, (Cor c) -> {
            return c.getRed() == r && c.getGreen() == g && c.getBlue() == b && c.getOpacity() == o;
        });

        if (filtrarEntidades.isEmpty()) {
            Cor c = new Cor(r, g, b, o);
            c.registarNovaEntidade();
            return c;
        }

        return filtrarEntidades.get(0);
    }

    @Override
    public Textura getTextura(int z, String caminho) {
        if (caminho == null) {
            throw new NullPointerException("caminho não pode ser null!");
        }

        Textura t = new Textura(z, caminho);
        t.registarNovaEntidade();
        return t;
    }

    @Override
    public void drag(float x, float y, boolean fim) {
        estado.setValue(estado.getValue().drag(x, y, fim));
    }

    @Override
    public void click(float x, float y, boolean ctr) {
        estado.setValue(estado.getValue().click(x, y, ctr));
    }

    @Override
    public void guardarFicheiro() {
        GestorDeEntidades.getInstance().commit();
        gestorComandos.clearAll();
    }

    @Override
    public ObjectProperty<Estado> estadoProperty() {
        return estado;
    }

    @Override
    public Grelha getGrelha() {
        return grelha;
    }

    @Override
    public IMoverEstrategia getMoverEstrategia() {
        return moverEstrategia;
    }

    @Override
    public void setMoverEstrategia(IMoverEstrategia moverEstrategia) {
        if (moverEstrategia == null) {
            throw new NullPointerException("moverEstrategia não pode ser null!");
        }
        this.moverEstrategia = moverEstrategia;
    }

    private static Sistema SINGLETON = null;

    public synchronized static ISistema getInstance() {
        if (SINGLETON == null) {
            Map<String, Familia> familias = GestorDeEntidades.getInstance().procurarPorTodasAsEntidadesComoMapa(Familia.class)
                    .values()
                    .stream()
                    .collect(Collectors.toMap((Familia f) -> {
                        return f.getNome();
                    }, (Familia f) -> {
                        return f;
                    }));

            Map<String, Estilo> estilos = GestorDeEntidades.getInstance().procurarPorTodasAsEntidadesComoMapa(Estilo.class)
                    .values()
                    .stream()
                    .collect(Collectors.toMap((Estilo e) -> {
                        return e.getNomeDoEstilo();
                    }, (Estilo e) -> {
                        return e;
                    }));

            SINGLETON = new Sistema(familias, estilos, FXCollections.observableMap(getTelhasDaBaseDeDados()));
        }
        return SINGLETON;
    }

    public static Map<KeyTelha, TelhaBase> getTelhasDaBaseDeDados() {
        Map<KeyTelha, TelhaBase> map = new HashMap<>();
        Map<Integer, Telha> telhas = GestorDeEntidades.getInstance().procurarPorTodasAsEntidadesComoMapa(Telha.class);
        Map<Integer, Decorador> decoradores = GestorDeEntidades.getInstance().procurarPorTodasAsEntidadesComoMapa(Decorador.class);

        telhas.forEach((Integer id, Telha telha) -> {
            TelhaBase tb = telha;
            while (true) {
                Decorador decorador = getDecorador(decoradores, tb);
                if (decorador != null) {
                    tb = decorador;
                } else {
                    break;
                }
            }
            map.put(new KeyTelha(tb.getId(), tb.getClass()), tb);
        });

        return map;
    }

    private static Decorador getDecorador(Map<Integer, Decorador> decoradores, TelhaBase tb) {
        List<Decorador> collect = decoradores.values().stream().filter((Decorador t) -> {
            return t.getTelhaDecorada() == tb;
        }).collect(Collectors.toList());
        if (collect.isEmpty()) {
            return null;
        } else {
            return collect.get(0);
        }
    }
}
